SnowflakeのJava UDFsを試してみた
こんにちは!DA(データアナリティクス)事業本部 インテグレーション部の大高です。
SnowflakeのPreview機能として、Java UDFsが公開されています。
UDF(ユーザー定義関数)としては、既にSQLやJavaScriptで作成できるUDFがありますが、新たに提供予定のJava UDFsでは、jarファイルを利用したUDFの作成ができるのが面白そうだと思ったので、実際に試してみました。
前提条件
以下を前提としています。
Java UDFsについて
Java UDFsはPreview機能として公開されていますが、2021年06月 現在では下記の制約があります。
- 利用できる環境はAWSにホスティングされているSnowflakeアカウント
今回はAWSにホスティングされているSnowflakeアカウントを利用します。
Javaの環境について
Javaの環境は下記のとおり、MacにAmazon Correttoを導入しています。
- OSは macOS Big Sur を利用中
- Amazon Corretto 11 を導入済み
なお、javac
を利用するには上記の手順に加えて、以下のようなパスの追加設定が必要です。
PATH=${JAVA_HOME}/bin:${PATH}
$ java --version openjdk 11.0.3 2019-04-16 LTS OpenJDK Runtime Environment Corretto-11.0.3.7.1 (build 11.0.3+7-LTS) OpenJDK 64-Bit Server VM Corretto-11.0.3.7.1 (build 11.0.3+7-LTS, mixed mode) $ javac --version javac 11.0.3
その他
Snowflakeの利用には、snowsql
を利用します。
作成するUDFについて
今回は「うるう年」を判定するUDFを作成してみたいと思います。仕様としては「西暦の年数を数値で渡したら、ブール値で結果を返してくれる(うるう年ならTrue)」というような関数とします。
例えば、以下のようなクエリであればTRUEが返ってくる、という感じです。
SELECT is_leap_year(2020);
Jarファイルを用意する
では、早速Javaのコードを作成してJarファイルを用意します。
下記のドキュメントによると「パブリックなクラス」および「パブリックなメソッド」を定義する必要があります。また、static
ではないメソッドでも利用可能ですが、その場合は「コンストラクタを定義しない」か、「コンストラクタの引数を0にする」という必要があるそうです。
今回はstatic
なメソッドを作成して、これをUDFから利用するようにしたいと思います。
ディレクトリ構成
最終的な構成は以下のようにします。今回はpackageを利用したいので、Manifestファイルも作成します。
. ├── HelloJavaUDF.jar ├── MANIFEST.MF ├── com │ └── hello │ └── HelloJavaUDF.class └── src └── com └── hello └── HelloJavaUDF.java
Javaファイル
Javaのコードは以下のようにします。isLeapYear
をUDFでの利用向けに作成しています。
package com.hello; public class HelloJavaUDF { public static boolean isLeapYear(int year){ // 4で割り切れるない場合はうるう年ではない if (year % 4 != 0){ return false; } // ただし、4で割り切れても、100で割り切れて400で割り切れない場合はうるう年でない if((year % 100 == 0) && (year % 400 != 0)){ return false; } return true; } public static void main(String args[]){ boolean isLeapYear = HelloJavaUDF.isLeapYear(Integer.valueOf(args[0])); System.out.println(isLeapYear ? "うるう年です" : "うるう年じゃないです"); } }
試しに動かすと、こんな感じです。
$ java src/com/hello/HelloJavaUDF.java 2020 うるう年です $ java src/com/hello/HelloJavaUDF.java 2021 うるう年じゃないです
Manifestファイル
Manifestファイルは以下のように定義します。
Manifest-Version: 1.0 Class-Path: . Main-Class: com.hello.HelloJavaUDF.class
この段階ではフォルダ構成はこのようになっています。
. ├── MANIFEST.MF └── src └── com └── hello └── HelloJavaUDF.java
コンパイル
用意ができたので、コンパイルします。
$ javac -d ./ ./src/com/hello/HelloJavaUDF.java
これで、com
配下にclassファイルが出力されます。
. ├── HelloJavaUDF.jar ├── MANIFEST.MF ├── com │ └── hello │ └── HelloJavaUDF.class └── src └── com └── hello └── HelloJavaUDF.java
Jarファイルの作成
最後に下記を参考にJarファイルを作成します。
- Creating a Pre-compiled Java UDF
現在の構成に従って、下記コマンドでHelloJavaUDF.jar
ファイルを作成します。
$ jar cmf MANIFEST.MF ./HelloJavaUDF.jar ./com/hello/HelloJavaUDF.class
これでjarファイルが用意できました。
. ├── HelloJavaUDF.jar ├── MANIFEST.MF ├── com │ └── hello │ └── HelloJavaUDF.class └── src └── com └── hello └── HelloJavaUDF.java
JarファイルをSnowflakeのユーザーステージにアップロードする
jarファイルが用意できたので、Snowflakeへアップロードします。今回はユーザステージにアップロードします。
snowsqlを起動して、PUTコマンドでアップロードします。
$ snowsql >PUT file://./HelloJavaUDF.jar @~/udf/; HelloJavaUDF.jar_c.gz(0.00MB): [##########] 100.00% Done (0.074s, 0.01MB/s). ╒══════════════════╤═════════════════════╤═════════════╤═════════════╤════════════════════╤════════════════════╤══════════╤═════════╕ │ source │ target │ source_size │ target_size │ source_compression │ target_compression │ status │ message │ ╞══════════════════╪═════════════════════╪═════════════╪═════════════╪════════════════════╪════════════════════╪══════════╪═════════╡ │ HelloJavaUDF.jar │ HelloJavaUDF.jar.gz │ 1073 │ 902 │ NONE │ GZIP │ UPLOADED │ │ ╘══════════════════╧═════════════════════╧═════════════╧═════════════╧════════════════════╧════════════════════╧══════════╧═════════╛ 1 Row(s) produced. Time Elapsed: 1.119s
UDFを作成する
jarファイルがアップロードできたのでUDFを作成します。snowsql上で、以下のようにして作成しました。
-- コンテキスト設定 USE DATABASE OOTAKA_SANDBOX_DB; USE SCHEMA PUBLIC; USE WAREHOUSE X_SMALL_WH; -- UDFを登録 CREATE FUNCTION is_leap_year(year NUMERIC(4, 0)) RETURNS BOOLEAN LANGUAGE java IMPORTS = ('@~/udf/HelloJavaUDF.jar') HANDLER = 'com.hello.HelloJavaUDF.isLeapYear' ;
成功すると、以下のように表示されます。USE WAREHOUSE
を指定しているのですが、WAREHOUSEを指定して起動できるようにすることで、UDFの作成時に併せて検証をしてくれるようです。
╒═════════════════════════════════════════════╕ │ status │ ╞═════════════════════════════════════════════╡ │ Function IS_LEAP_YEAR successfully created. │ ╘═════════════════════════════════════════════╛ 1 Row(s) produced. Time Elapsed: 3.482s
なお、アクティブなWAREHOUSEが無いと、以下のように表示されます。
╒═════════════════════════════════════════════════════════════════════════════════════════════════════════════╕ │ status │ ╞═════════════════════════════════════════════════════════════════════════════════════════════════════════════╡ │ Function IS_LEAP_YEAR successfully created, but could not be validated since there is no active warehouse. │ ╘═════════════════════════════════════════════════════════════════════════════════════════════════════════════╛
作成時に困ったこと
実は最初はjarの作成時にclassファイルを./classes/
ディレクトリ以下に配置して、Manifestファイルを以下のように定義していました。
Manifest-Version: 1.0 Class-Path: ./classes/ Main-Class: com.hello.HelloJavaUDF.class
構成としては以下のような感じです。
. ├── HelloJavaUDF.jar ├── MANIFEST.MF ├── classes │ └── com │ └── hello │ └── HelloJavaUDF.class └── src └── com └── hello └── HelloJavaUDF.java
この状態でUDFを作成しようとすると、クラスパス上に該当クラスが見つからない旨のエラーが発生していました。私のJavaとManifestファイルの理解が浅いので設定が良くないのかもしれませんが、同じ事象の場合には一旦クラスファイルは階層を掘らずに作成するとうまくいくかもしれません。
100315 (P0000): User Error Report: Java Stack Trace: java.lang.ClassNotFoundException: com.hello.HelloJavaUDF at java.base/java.net.URLClassLoader.findClass(URLClassLoader.java:471) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:589) at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:522) in function IS_LEAP_YEAR with handler com.hello.HelloJavaUDF.isLeapYear
いざ、UDFを実行する
これで準備が整いましたので、実際にUDFを呼び出してみましょう。
SELECT is_leap_year(2020);
╒════════════════════╕ │ IS_LEAP_YEAR(2020) │ ╞════════════════════╡ │ True │ ╘════════════════════╛ 1 Row(s) produced. Time Elapsed: 0.795s
想定どおり、True
が取得できました!一応、2021
年でも試してみましょう。
SELECT is_leap_year(2021);
╒════════════════════╕ │ IS_LEAP_YEAR(2021) │ ╞════════════════════╡ │ False │ ╘════════════════════╛ 1 Row(s) produced. Time Elapsed: 0.852s
良さそうですね。
まとめ
以上、SnowflakeのJava UDFを試してみました。
今回はシンプルなJavaのクラスで試してみたのですが、jarファイルを利用できるというのはとても良いなと感じました。ソースコードをローカル側で管理もできますし、jarであれば色々応用も効きそうですね。
どなたかのお役に立てば幸いです。それでは!